跳到主要内容

JavaScript Promise 异步编程

为什么需要异步编程?

因为 JavaScript 大部分时候都是单线程,如果这时发生 IO 密集型和 CPU 密集型的任务需要处理时就会发生阻塞,前面讲过的 Web Workers 虽然也可以执行多任务的操作,但是它有个致命的缺点就是无法操作 DOM 元素,其次,虽然在 worker 里面运行的代码不会产生阻塞,但是基本上还是同步的。当一个函数依赖于几个在它之前运行的过程的结果,这就会成为问题。

异步 仅仅是推迟了某些代码的执行,对于一段需要长时间来执行的代码来说,异步只是将其的执行顺序移到后面,然后不阻塞其他的逻辑代码,等到其他代码执行完成,然后再执行这段代码,但是这段代码在执行的时候仍然还是会阻塞UI和其他业务代码 ,所以 CPU 密集型的操作还是需要靠 Web Workers

异步编程更适合一些 I/O操作 使CPU充分利用起来

在 JavaScript 代码中,有两种异步编程风格:老派callbacks新派promise

CallBacks 的方式

参考资料 MDN--合作异步JavaScript

异步callbacks 其实就是回调函数,作为参数传递给那些在后台执行的其他函数. 当那些后台运行的代码结束,就调用callbacks函数

例如 JavaScript原生的事件系统传入的就是一个回调函数

// 第一个参数是侦听的事件类型,第二个就是事件发生时调用的回调函数。

btn.addEventListener('click', () => {
alert('You clicked me!');

let pElem = document.createElement('p');
pElem.textContent = 'This is a newly-added paragraph.';
document.body.appendChild(pElem);
});

Web提供给Javascript的一些异步代码

setTimeout() :在指定的时间后执行一段代码.

let myGreeting = setTimeout(function() {
alert('Hello, Mr. Universe!');
}, 2000)

setInterval():以固定的时间间隔,重复运行一段代码.

function displayTime() {
let date = new Date();
let time = date.toLocaleTimeString();
document.getElementById('demo').textContent = time;
}

const createClock = setInterval(displayTime, 1000);

Promises 的方式

参考资料 MDN--优雅的异步处理

Promises 的三个状态

创建 Promises 时,它既不是成功也不是失败状态。这个状态叫作 pending(待定) 当 Promises 返回时,称为 resolved(已解决)。

  • 一个成功的 resolved 称为 fullfilled(实现)。可以将 .then() 块链接到 Promises 链的末尾来链式调用
  • 一个不成功的 resolved 被称为 rejected(拒绝)了。它返回一个错误原因(reason),同样可以把 .catch() 写到最后面用来捕获异常

状态改变只有两种可能

  • 从 pending 变成 fullfilled
  • 从 pending 变成 rejected

Promise 传递只要没有改变状态,状态是不会变的

function f1() {
return Promise.resolve(100)
}

f1().then(resolve => {
console.log(resolve);
}).then(resolve => {
console.log('还能打印吗?');
console.log(resolve); // 注意看这个是否输出了
})

/*
输出为:
100
还能打印吗?
*/

只要不去更改 then 的状态为 rejected,then 会一直传递下去(但是传的值不会传递下去)

Promise 主要的三个 API

  • p.then() 得到异步任务的正确结果
  • p.catch() 获取异常信息
  • p.finally() 成功与否都会执行

如何使用?

let promise = new Promise(传入一个函数);

// 注意:promise构造函数是同步执行的,then方法是异步执行的
let promise = new Promise((resolve,reject)=>{
if( 异步请求操作成功 ){
// 这个resolve用来传递参数
// 只要执行了这个 resolve 状态就变成了完成,下面的reject同理
resolve(value);
} else{
reject(error);
}
})

// 要等上面创建时传入的回调函数执行完了才会执行下面的then
promise.then(res =>{
console.log(res) //成功
}).catch(err =>{
console.log(err) //失败
})

then 的返回值

1、返回 Promise 实例对象

.then((res)=>{
return new Promise((resolve,reject)=>{
...
})
})

返回的该实例对象会调用下一个 then

2、返回普通值

.then((res)=>{return "hello promise"})

返回的普通值会直接传递给下一个 then,通过 then 参数中函数的参数接收值(实际上 then 函数会自动封装一个 Promise.resolve 对象出去,并把普通值传入 resolve 里)

Promise 包装多个任务

Promise.all([
$.ajax({ url: 'data/1.txt', dataType: 'json' }),
$.ajax({ url: 'data/2.txt', dataType: 'json' }),
$.ajax({ url: 'data/3.txt', dataType: 'json' })
]).then(arr => {
let [data1, data2, data3] = arr; //结构赋值
console.log(data1, data2, data3);
}, () => {
alert('失败了');
});

Promise.all 可以将多个 Promise 实例包装成一个新的 Promise 实例。同时返回值也是不同的,成功时返回的是一个结果数组,而失败时返回最先被 reject 失败状态的值

let p1 = new Promise((resolve, reject) => {
reject('成功了')
});

let p2 = new Promise((resolve, reject) => {
reject('success')
});

let p3 = Promise.reject('失败')

Promise.all([p1, p2]).then(resolve => {
console.log(resolve)
}).catch(err => {
console.log(err);
})
// 返回值为 ['成功','success'] 注意是数组!

Promise.all([p1, p2, p3]).then(resolve => {
console.log(resolve)
}).catch(err => {
console.log(err);
})
// 返回值为 '失败'

async/await

也是用来处理异步的,是 Generator 函数的改进,原理就是 Promise

一般这个 await 关键字就是用来处理 多个异步请求,下一个请求需要依赖上一个请求的结果

所以:await 关键字的作用就是把异步的请求转成同步,然后这个 async 把里面的操作整合成一个大的异步请求

注意看下面的例子

async function test() {
let a = await new Promise((res,rej)=>{
setTimeout(()=>{
console.log("执行完了 A");
// 必须把 res 状态传递出去,否则会一直阻塞在这里
res('A is OK!')
},2000);
})

let b = await new Promise((res,rej)=>{
setTimeout(()=>{
console.log("执行完了 B");
res('B is OK!')
},1000);
})
return b;
}

test(); // 这个函数是异步的
console.log("这里是 C");

// 执行结果 :
// 这里是 C
// 执行完了 A
// 执行完了 B

// 看上面的结果可以知道,await 实际就是把异步请求转成同步了(A 的等待时间是两秒,B 的才一秒)

也可以直接搭配 axios 使用

async function queryData(id) {
const info = await axios.get('/async1');
// 下一个请求要用到上一个请求的结果
const ret = await axios.get(`/async2?info=${info.data}`);
}

async/await 捕获异常

如果返回的是一个 reject 则需要捕获,由于 try-catch 只能用于同步代码里,所以必须加上 await,还有一点就是 await 只能用于 async 函数

async function f1(){
return Promise.reject('这是一个错误')
}

// 如果直接调用会报错
f1()

//需要使用如下的方式捕获这个异常
// try catch 只能用于同步代码里,所以必须给 f1() 加上 await
// await只能用于 async 函数 所以..
async function f2(){
try {
await f1()
} catch (e) {
console.log(e)
}
}

// 调用 f2() 来捕获 f1() 的异常
f2()

References